<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

// =============================================================================
// Section: Documentation
// =============================================================================

Aero::define('AeroDoc', AS_CLASS);

define('T_MISC',0);

/* -----------------------------------------------------------------------------
 * Class: AeroDoc
 * -----------------------------------------------------------------------------
 *
 */
class AeroDoc
{
	protected $path;
	protected $name;
	protected $manifest;
	protected $navigation;
	protected $pages;
	protected $files;
	
	public function __construct($path)
	{
		// TODO: Should fail if path is not a writable directory.
		$this->path = $path;
	}
	
	public function chapter($name, $manifest = null)
	{
		$this->save();
		
		$this->name = $name;
		$this->manifest = is_null($manifest) ? 
			strtolower(str_replace(array(' ','-'), '_', $name)) : $manifest;
		
		$this->pages = array();
		$this->files = array();
	}
	
	public function save()
	{
		if (is_null($this->name))
		{
			return;
		}
		
		
	}
	
	public function process($dir_path)
	/*
	 * Creates documentation from all files within specified directory.
	 */
	{
		if (!file_exists($dir_path) || !is_dir($dir_path))
		{
			trigger_error('Invalid directory path: <kbd>'.$dir_path.'</kbd>.');
		}
		
		// Directories that contain files
		$dirs = array($dir_path);
		
		// FIXME: Does it only document 2 levels deep???
		// Search through all (sub)directories
		while (count($dirs) > 0)
		{
			$path = array_shift($dirs);
			
			$handler = opendir($path);
			
			// Read and parser all files
			while ($file = readdir($handler))
			{
				if ($file == '.' || $file == '..')
				{
					continue;
				}
				
				$subpath = rtrim($path,'/').'/'.$file;
				
				// Store directory paths to be handled later
				if (is_dir($subpath))
				{
					$dirs[] = $subpath;
					continue;
				}
				
				$parts = explode('.',$file);
				$ext = array_pop($parts);
				$relative_path = str_replace($dir_path, '', $subpath);
				
				if (in_array($ext,array('php','inc')))
				{
					// Parse files
					$file = $this->_processFile($subpath);
					$this->docs[trim($relative_path,'/')] = $this->processFile($file);
					
				}
				else if (in_array($ext,array('txt','md')) || count($parts) == 0)
				{
					// Simply markdown pages
					$page = $this->_processPage($subpath);
					$this->pages[trim($relative_path,'/')] = $this->processPage($page);
				}
			}
		}
		
		$md = $this->manifest.'.md';
		$txt = $this->manifest.'.txt';
		
		if (isset($this->pages[$md]) || isset($this->pages[$txt]))
		{
			$name = isset($this->pages[$md]) ? $md : $txt;
			
			// TODO: Extract navigation and introductory contents from the manifest.
			
			unset($this->pages[$name]);
		}
	}
	
	protected function processFile($file)
	{
		return $file;
	}
	
	protected function processPage($page)
	{
		return $page;
	}
	
	// ======================
	// = File parsing logic =
	// ======================
	
	private $file_tokens;
	private $file_token_offset;
	
	// We document only constant, function and class definitions
	private $file_constants;
	private $file_functions;
	private $file_classes;
	
	// protected $scope_namespace;
	private $scope_class;
	private $scope_function;
	private $scope_level;
	
	private $definition_prefix;
	
	private $last_comment_type;
	private $last_comment;
	private $last_whitespace;
	
	protected function _processFile($file_path)
	/*
	 * Parses a php and attempts to document all elements, 
	 * grab code snippets and do other nasty things.
	 */
	{
		// Load source code
		$source = file_get_contents($file_path);
		
		// Normalize line endings
		$source = str_replace("\r\n","\n",$source);
		$source = str_replace("\r","\n",$source);
		
		// Reset file info
		$this->file_tokens = token_get_all($source);
		$this->file_token_offset = 0;
		$this->file_functions = array();
		$this->file_classes = array();
		$this->definition_prefix = array();
		$this->last_comment = null;
		$this->last_comment_type = null;
		$this->last_whitespace = null;
		$this->scope_class = null;
		$this->scope_function = null;
		$this->scope_level = 0;
		
		// TODO: Drop the system part of the file path
		// echo '<h2>'.$file_path.'</h2>';
		
		while ($this->_hasTokens())
		{
			list($tType,$tContent) = $this->_nextToken();
			
			#
			# Scope control
			#
			
			if ($tType === T_MISC && $tContent === '{')
			{
				$this->scope_level++;
				$this->_onScopeEnter();
			}
			else if ($tType === T_MISC && $tContent === '}')
			{
				$this->scope_level--;
				$this->_onScopeLeave();
			}
			
			#
			# Search for class, function, variable and constant definitions
			#
			
			// Remember keywords which usually preceed class, method and property definitions.
			if (in_array($tType, array(T_STATIC, T_VAR, T_CONST, T_ABSTRACT, T_FINAL, T_PRIVATE, T_PUBLIC, T_PROTECTED),true))
			{
				$this->definition_prefix[$tType] = $tContent;
				continue;
			}
			// Parse 'class' and 'function' definitions. Interfaces are sort of classes in this case
			else if (in_array($tType, array(T_CLASS, T_FUNCTION, T_INTERFACE)))
			{
				$this->definition_prefix[$tType] = $tContent;
				$this->_onBlockDefinition();
				$this->_clearAfterDefinition();
				continue;
			}
			else if (count($this->definition_prefix) > 0 && in_array($tType, array(T_VARIABLE, T_STRING)))
			{
				$this->_preserveToken();
				$this->_onInlineDefinition();
				$this->_clearAfterDefinition();
				continue;
			}
			else if (!empty($this->definition_prefix) && !in_array($tType,array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT)))
			{
				$this->definition_prefix = array();
			}
			
			#
			# Keep the last block of comments available for definitions
			# to grab and use.
			#
			if (in_array($tType,array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT)))
			{
				// Detect comment type
				if ($tType === T_COMMENT)
				{
					if (strpos($tContent, '#') === 0)
					{
						$comment_type = '#';
					}
					else if (strpos($tContent, '//') === 0)
					{
						$comment_type = '//';
					}
					else
					{
						$comment_type = '/*';
						$this->last_comment = null;
					}
				}
				else if ($tType === T_DOC_COMMENT)
				{
					$comment_type = '/**';
					$this->last_comment = null;
				}
				
				if ($tType === T_WHITESPACE && substr_count($tContent, "\n") > 1)
				{
					$this->last_comment = null;
					$this->last_whitespace = substr($tContent, strrpos($tContent, "\n"));
				}
				else
				{
					// Prevent comment type mixing
					if (isset($comment_type) && $this->last_comment_type !== $comment_type)
					{
						$this->last_comment = $this->last_whitespace . $tContent;
					}
					else
					{
						$this->last_comment.= $this->last_whitespace . $tContent;
						
					}
					$this->last_whitespace = null;
					
					if (isset($comment_type))
					{
						$this->last_comment_type = $comment_type;
					}
				}
				
				continue;
			}
			
		}
		
		// Create documentation for all found elements
		// var_dump($this->file_classes);
		// var_dump($this->file_functions);
		echo '<pre>'.print_r($this->file_classes,true).'</pre>';
		echo '<pre>'.print_r($this->file_functions,true).'</pre>';
		
		// exit;
	}
	
	protected function _hasTokens()
	{
		return ($this->file_token_offset < count($this->file_tokens));
	}
	
	protected function _nextToken()
	{
		static $line_number;
		
		$offset = $this->file_token_offset++;
		$token = $this->file_tokens[$offset];

		if (!is_array($token))
		{
			// var_dump($token);
			return array(T_MISC, $token, $line_number);
		}
		
		$line_number = $token[2] + substr_count($token[1], "\n");
		// $_token = $token;
		// $_token[0] = token_name($token[0]);
		// var_dump($_token);
		
		return $token;
	}
	
	protected function _preserveToken()
	{
		$this->file_token_offset--;
	}
	
	protected function _onInlineDefinition()
	{
		$is_const = isset($this->definition_prefix[T_CONST]);
		$definition = $this->definition_prefix;
		$var = null;
		$value = null;
		$vars = array();
		
		while ($this->_hasTokens())
		{
			list($tType,$tContent) = $this->_nextToken();
			
			if ($tType === T_MISC && $tContent === ';')
			{
				break;
			}
			
			if ($tType === T_VARIABLE || $tType === T_STRING)
			{
				$var.= self::_highlightToken($tType,$tContent);
				$value = $this->_findValueAssignmentBreakingOn(array(',' => T_MISC, ';' => T_MISC));
				$vars[] = array($var,$value);
			}
			
			$var = null;
			$value = null;
		}
		
		if (count($vars) && !is_null($this->scope_class))
		{
			$class = $this->scope_class['name'];
			$type = $is_const ? 'constants' : 'properties';
			
			foreach ($vars as $var)
			{
				$name = $var[0];
				
				$this->file_classes[$class][$type][$name] = array(
					'definition' => $definition,
					'name' => $var[0],
					'value' => $var[1],
					'comment_before' => $this->_commentBefore()
				);
				
				if (count($vars) === 1)
				{
					$this->file_classes[$class][$type][$name]['comment_after'] = $this->_commentAfter();
				}
			}
		}
	}
	
	protected function _onBlockDefinition()
	{
		$is_function = isset($this->definition_prefix[T_FUNCTION]);
		$definition = $this->definition_prefix;
		$name = null;
		$parameters = array();
		$implements = array();
		$extends = array();
		
		while ($this->_hasTokens())
		{
			list($tType,$tContent) = $this->_nextToken();
			
			if ($tType === T_MISC && $tContent === ';')
			{
				break;
			}
			else if ($tType === T_MISC && $tContent === '{')
			{
				if ($is_function)
				{
					$this->scope_function['name'] = $name;
					$this->scope_function['scope_level'] = $this->scope_function;
				}
				else
				{
					$this->scope_class['name'] = $name;
					$this->scope_class['scope_level'] = $this->scope_level;
				}
				
				$this->scope_level++;
				break;
			}
			
			if ($tType === T_STRING)
			{
				$name = $tContent;
				continue;
			}
			
			if ($tType === T_IMPLEMENTS)
			{
				$implements = $this->_parseCommaSeparatedStrings();
				continue;
			}
			
			if ($tType === T_EXTENDS)
			{
				$extends = $this->_parseCommaSeparatedStrings();
				continue;
			}
			
			if ($is_function && $tType === T_MISC && $tContent === '(')
			{
				$parameters = $this->_parseFunctionParameters();
				continue;
			}
		}
		
		if ($is_function)
		{
			$function = array(
				'definition' => $definition,
				'name' => $name,
				'parameters' => $parameters,
				'comment_before' => $this->_commentBefore()
			);
			
			if (is_null($this->scope_class))
			{
				$this->file_functions[$name] = $function;
			}
			else
			{
				$class = $this->scope_class['name'];
				
				$this->file_classes[$class]['methods'][$name] = $function;
			}
		}
		else
		{
			$this->file_classes[$name] = array(
				'definition' => $definition,
				'name' => $name,
				'implements' => $implements,
				'extends' => $extends,
				'comment_before' => $this->_commentBefore()
			);
		}
	}
	
	protected function _clearAfterDefinition()
	{
		$this->definition_prefix = array();
		$this->last_comment = null;
		$this->last_whitespace = null;
	}
	
	protected function _parseCommaSeparatedStrings()
	{
		$strings = array();
		
		while ($this->_hasTokens())
		{
			list($tType,$tContent) = $this->_nextToken();
			
			if ($tType === T_MISC && $tContent === ',' || $tType === T_WHITESPACE)
			{
				continue;
			}
			else if ($tType === T_STRING)
			{
				$strings[] = $tContent;
			}
			else
			{
				$this->_preserveToken();
				return $strings;
			}
		}
		
		return $strings;
	}

	protected function _parseFunctionParameters()
	{
		$parameters = array();
		$var = null;
		$value = null;
		
		while ($this->_hasTokens())
		{
			list($tType,$tContent) = $this->_nextToken();
			
			if ($tType === T_MISC && $tContent === ')')
			{
				if (!is_null($var))
				{
					$parameters[] = array($var, $value);
				}
				
				return $parameters;
			}
			
			// Capture parameter variable optionally prefixed by ampersand
			if (is_null($var) && $tType === T_MISC && $tContent === '&')
			{
				$var.= self::_highlightToken($tType,$tContent);
				continue;
			}
			
			if ($tType === T_VARIABLE)
			{
				$var.= self::_highlightToken($tType,$tContent);
				$value = $this->_findValueAssignmentBreakingOn(array(',' => T_MISC));
				$parameters[] = array($var,$value);
			}
			
			$var = null;
			$value = null;
		}
		
		return $parameters;
	}
	
	protected function _findValueAssignmentBreakingOn($break_on)
	{
		$assignment_found = false;
		$value = null;
		$depth = 0;
		
		while ($this->_hasTokens())
		{
			list($tType,$tContent) = $this->_nextToken();
			
			if ($depth === 0 && isset($break_on[$tContent]) && $break_on[$tContent] === $tType)
			{
				$this->_preserveToken();
				return $value;
			}
			
			// Watch out for the parenthesis!
			if ($tType === T_MISC && $tContent === '(')
			{
				$depth++;
			}
			else if ($tType === T_MISC && $tContent === ')')
			{
				if ($depth === 0)
				{
					// Oops, we are running away!
					$this->_preserveToken();
					return $value;
				}
				
				$depth--;
			}
			
			// Find assignment token first
			if (!$assignment_found)
			{
				if ($tType === T_MISC && $tContent === '=')
				{
					$assignment_found = true;
					continue;
				}
				
				continue;
			}
			
			// Ignore whitespace
			if ($tType === T_WHITESPACE && $depth === 0)
			{
				continue;
			}
			
			$value.= $this->_highlightToken($tType, $tContent);
		}
		
		return $value;
	}
	
	protected function _onScopeEnter()
	{
		
	}
	
	protected function _onScopeLeave()
	{
		if (is_null($this->scope_function) && is_null($this->scope_class))
		{
			return;
		}
		
		if ($this->scope_class['scope_level'] === $this->scope_level)
		{
			$this->scope_class = null;
		}
		else if ($this->scope_function['scope_level'] === $this->scope_level)
		{
			$this->scope_function = null;
		}
	}
	
	protected function _commentBefore()
	{
		if (is_null($this->last_comment))
		{
			return null;
		}
		
		// Get comment, replacing tabs with spaces in a very crude way
		// Really, you should stick to either, folks!
		$comment = str_replace("\t", '    ', $this->last_comment);
		$type = $this->last_comment_type;
		
		if ($type === '#' || $type === '//')
		{
			$comment = preg_replace("~^[ ]*{$type}~m", '', $comment);
		}
		// Block comment should be at least three lines because
		// '/*' and '*/' should be on separate lines
		else if (substr_count($comment, "\n") > 2)
		{
			$comment = preg_replace(array(
				'~^[\t \-_=\*\.\\/]+$~m', // Clean lines with no text
			), '', $comment);
		}
		else
		{
			return null; // invalid comment block
		}
		
		// Find the shortest prefix non-word on each non-empty line
		// and trim by it. This way we eliminiate any decoration that
		// preceds text on each line.
		preg_match_all("~^[^\n\w]+~m", $comment, $suffixes);
		$length = min(array_map('strlen', $suffixes[0]));
		$comment = preg_replace(array(
				'~^[^\n]{'.$length.'}(.*)$~m', // Trim any preceding decoration...
				"~^\n+~", // ...and empty lines before...
				"~\n+$~", // ...and after.
			),
			array('\1','',''),
			$comment);
		
		return $comment;
	}
	
	protected function _commentAfter()
	{
		$_offset = $this->file_token_offset;
		
		while ($this->_hasTokens())
		{
			list($tType, $tContent, $line) = $this->_nextToken();
			
			if ($tType === T_WHITESPACE && strpos($tContent, "\n") !== false)
			{
				$this->file_token_offset = $_offset;
				return null;
			}
			
			if ($tType === T_COMMENT)
			{
				return preg_replace('~^(?:#|//|(/\*))(.*?)(?(1)\*/)?$~','\2',$tContent);
			}
		}
		
		$this->file_token_offset = $_offset;
		return null;
	}
	
	static public function _highlightToken($type, $content)
	{
		// return '<code class="'.token_name($type).'">'.$content.'</code>';
		return $content;
	}
	
	// ======================
	// = Page parsing logic =
	// ======================
	
	protected function _processPage($file_path)
	{
		
	}
}